/**
 * HTTP client for ComfyUI API interactions
 */

import type {
  ComfyUIClientConfig,
  QueuePromptRequest,
  QueuePromptResponse,
  QueueStatus,
  History,
  SystemStats,
  ObjectInfo,
  APIClient
} from '../types';

/**
 * HTTP client for ComfyUI REST API
 */
export class HTTPClient implements APIClient {
  private readonly baseUrl: string;
  private readonly timeout: number;
  private readonly retryAttempts: number;
  private readonly retryDelay: number;
  private readonly headers: Record<string, string>;

  /**
   * Creates a new HTTPClient instance
   * @param config - Client configuration
   */
  constructor(config: ComfyUIClientConfig) {
    this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
    this.timeout = config.timeout || 30000; // 30 seconds default
    this.retryAttempts = config.retryAttempts || 3;
    this.retryDelay = config.retryDelay || 1000; // 1 second default
    this.headers = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      ...config.headers
    };
  }

  /**
   * Queues a prompt for execution
   * @param request - Queue prompt request
   * @returns Promise resolving to queue response
   */
  async queuePrompt(request: QueuePromptRequest): Promise<QueuePromptResponse> {
    const response = await this.request<QueuePromptResponse>('POST', '/prompt', request);
    return response;
  }

  /**
   * Gets the current queue status
   * @returns Promise resolving to queue status
   */
  async getQueue(): Promise<QueueStatus> {
    const response = await this.request<QueueStatus>('GET', '/queue');
    return response;
  }

  /**
   * Gets execution history
   * @param promptId - Optional specific prompt ID to get history for
   * @returns Promise resolving to history
   */
  async getHistory(promptId?: string): Promise<History> {
    const url = promptId ? `/history/${promptId}` : '/history';
    const response = await this.request<History>('GET', url);
    return response;
  }

  /**
   * Gets system statistics
   * @returns Promise resolving to system stats
   */
  async getSystemStats(): Promise<SystemStats> {
    const response = await this.request<SystemStats>('GET', '/system_stats');
    return response;
  }

  /**
   * Gets object information (available nodes and their schemas)
   * @returns Promise resolving to object info
   */
  async getObjectInfo(): Promise<ObjectInfo> {
    const response = await this.request<ObjectInfo>('GET', '/object_info');
    return response;
  }

  /**
   * Interrupts the current execution
   * @returns Promise resolving when interrupt is sent
   */
  async interrupt(): Promise<void> {
    await this.request<void>('POST', '/interrupt');
  }

  /**
   * Clears the execution queue
   * @returns Promise resolving when queue is cleared
   */
  async clearQueue(): Promise<void> {
    await this.request<void>('POST', '/queue', { clear: true });
  }

  /**
   * Gets an image from the server
   * @param filename - Image filename
   * @param subfolder - Optional subfolder
   * @param type - Image type (output, input, temp)
   * @returns Promise resolving to image blob
   */
  async getImage(filename: string, subfolder?: string, type: string = 'output'): Promise<Blob> {
    const params = new URLSearchParams({
      filename,
      type
    });

    if (subfolder) {
      params.append('subfolder', subfolder);
    }

    const url = `/view?${params.toString()}`;
    const response = await this.requestRaw('GET', url);

    if (!response.ok) {
      throw new Error(`Failed to get image: ${response.status} ${response.statusText}`);
    }

    return response.blob();
  }

  /**
   * Gets the HTTP URL for an image
   * @param filename - Image filename
   * @param subfolder - Optional subfolder
   * @param type - Image type (output, input, temp)
   * @returns Full HTTP URL to the image
   */
  getImageUrl(filename: string, subfolder?: string, type: string = 'output'): string {
    const params = new URLSearchParams({
      filename,
      type
    });

    if (subfolder) {
      params.append('subfolder', subfolder);
    }

    return `${this.baseUrl}/view?${params.toString()}`;
  }

  /**
   * Uploads an image to the server
   * @param file - File to upload
   * @param subfolder - Optional subfolder
   * @param overwrite - Whether to overwrite existing file
   * @returns Promise resolving to upload result
   */
  async uploadImage(
    file: File | Blob,
    subfolder?: string,
    overwrite: boolean = false
  ): Promise<{ name: string; subfolder: string }> {
    const formData = new FormData();
    formData.append('image', file);

    if (subfolder) {
      formData.append('subfolder', subfolder);
    }

    if (overwrite) {
      formData.append('overwrite', 'true');
    }

    const response = await this.requestRaw('POST', '/upload/image', formData);

    if (!response.ok) {
      throw new Error(`Failed to upload image: ${response.status} ${response.statusText}`);
    }

    return response.json() as Promise<{ name: string; subfolder: string }>;
  }

  /**
   * Uploads an image from a local file path (Node.js only)
   * @param filePath - Local file path to upload
   * @param subfolder - Optional subfolder
   * @param overwrite - Whether to overwrite existing file
   * @returns Promise resolving to upload result
   */
  async uploadImageFromPath(
    filePath: string,
    subfolder?: string,
    overwrite: boolean = false
  ): Promise<{ name: string; subfolder: string }> {
    // Check if we're in Node.js environment
    if (typeof window !== 'undefined') {
      throw new Error('uploadImageFromPath is only available in Node.js environment');
    }

    try {
      // Dynamic import for Node.js modules
      const fs = await import('fs');
      const path = await import('path');

      // Check if file exists
      if (!fs.existsSync(filePath)) {
        throw new Error(`File not found: ${filePath}`);
      }

      // Read file and create blob
      const fileBuffer = fs.readFileSync(filePath);
      const fileName = path.basename(filePath);

      // Create a Blob-like object for Node.js
      const blob = new Blob([fileBuffer], {
        type: this.getMimeType(fileName)
      });

      // Add filename property to blob for FormData
      Object.defineProperty(blob, 'name', {
        value: fileName,
        writable: false
      });

      return this.uploadImage(blob, subfolder, overwrite);
    } catch (error) {
      throw new Error(`Failed to upload image from path: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Get MIME type based on file extension
   * @private
   */
  private getMimeType(fileName: string): string {
    const ext = fileName.toLowerCase().split('.').pop();
    const mimeTypes: Record<string, string> = {
      'jpg': 'image/jpeg',
      'jpeg': 'image/jpeg',
      'png': 'image/png',
      'gif': 'image/gif',
      'webp': 'image/webp',
      'bmp': 'image/bmp',
      'tiff': 'image/tiff',
      'tif': 'image/tiff'
    };
    return mimeTypes[ext || ''] || 'application/octet-stream';
  }

  /**
   * Makes an HTTP request with JSON handling
   * @private
   * @param method - HTTP method
   * @param path - API path
   * @param body - Request body
   * @returns Promise resolving to response data
   */
  private async request<T>(
    method: string,
    path: string,
    body?: unknown
  ): Promise<T> {
    const response = await this.requestRaw(method, path, body);
    
    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`HTTP ${response.status}: ${errorText}`);
    }
    
    // Handle empty responses
    const contentType = response.headers.get('content-type');
    if (!contentType || !contentType.includes('application/json')) {
      return undefined as T;
    }
    
    return response.json() as Promise<T>;
  }

  /**
   * Makes a raw HTTP request with retry logic
   * @private
   * @param method - HTTP method
   * @param path - API path
   * @param body - Request body
   * @returns Promise resolving to raw response
   */
  private async requestRaw(
    method: string,
    path: string,
    body?: unknown
  ): Promise<Response> {
    const url = `${this.baseUrl}${path}`;
    
    for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.timeout);
        
        const requestInit: RequestInit = {
          method,
          headers: body instanceof FormData ? {} : this.headers,
          signal: controller.signal
        };
        
        if (body !== undefined) {
          if (body instanceof FormData) {
            requestInit.body = body;
          } else {
            requestInit.body = JSON.stringify(body);
          }
        }
        
        const response = await fetch(url, requestInit);
        clearTimeout(timeoutId);
        
        // Don't retry on client errors (4xx)
        if (response.status >= 400 && response.status < 500) {
          return response;
        }
        
        // Retry on server errors (5xx) or network errors
        if (response.ok || attempt === this.retryAttempts) {
          return response;
        }
        
      } catch (error) {
        // Don't retry on the last attempt
        if (attempt === this.retryAttempts) {
          throw error;
        }
        
        // Handle abort errors
        if (error instanceof Error && error.name === 'AbortError') {
          throw new Error(`Request timeout after ${this.timeout}ms`);
        }
      }
      
      // Wait before retrying
      if (attempt < this.retryAttempts) {
        await this.delay(this.retryDelay * Math.pow(2, attempt)); // Exponential backoff
      }
    }
    
    throw new Error('Max retry attempts reached');
  }

  /**
   * Delays execution for the specified time
   * @private
   * @param ms - Milliseconds to delay
   * @returns Promise that resolves after the delay
   */
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}
